import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Hashtable;
import java.util.LinkedList;

public class LLgenerator {
	private LLtable parseTable;
	private LinkedList<nonterminal> nonterminals;
	private LinkedList<terminal> terminals;
	private Hashtable<String, nonterminal> nonterminalHT;
	private Hashtable<String, terminal> terminalHT;

	public LLgenerator(String[] terminals, String[] nonterminals, String start)
	{
		this.nonterminalHT = new Hashtable<String, nonterminal>();
		this.terminalHT = new Hashtable<String, terminal>();
		this.nonterminals = new LinkedList<nonterminal>();
		this.terminals = new LinkedList<terminal>();

		for(String terminal : terminals){
			if (terminal.equals(epsilon.getEpsilonRule().getSymbol()))
				this.terminalHT.put(terminal, epsilon.getEpsilonRule());
			else
				this.terminalHT.put(terminal, new terminal(terminal));
		}

		for(String nonterminal : nonterminals){
			this.nonterminalHT.put(nonterminal, new nonterminal(nonterminal));
		}
		terminal dollarRule = new terminal("$");
		this.terminalHT.put("$", dollarRule);
		nonterminal startingRule = this.nonterminalHT.get(start);
		startingRule.getFollow().add(dollarRule);	
		this.parseTable = new LLtable(startingRule, dollarRule);
	}

	public void addRule(String symbol, String[] productionRuleStrings){
		if( !this.nonterminalHT.containsKey(symbol) ) // the symbol doesn't have a grammar rule yet 
		{
			throw new RuntimeException("\n Error Parsing Rules. \n" +
					"Symbol " + symbol + " is not in the grammar.");
		}
		else if(symbol.equals("$"))
		{
			throw new RuntimeException("\n Error Parsing Rules. \n" +
					"Symbol $ is a reserved symbol.");
		}

		// generate a production rule for the new symbol
		LinkedList<Rule> productionRule = new LinkedList<Rule>(); 
		for(String productionRuleString : productionRuleStrings)
		{
			if(this.nonterminalHT.containsKey(productionRuleString))
				productionRule.add(this.nonterminalHT.get(productionRuleString));
			else if(this.terminalHT.containsKey(productionRuleString))
				productionRule.add(this.terminalHT.get(productionRuleString));				
			else
				throw new RuntimeException("\n Error Parsing Rules. \n" 
						+ "Symbol " + productionRuleString + " is not in the grammar.");
		}
		// add the production rule to the symbol's grammar rule
		this.nonterminalHT.get(symbol).addProductionRule(productionRule);
	}

	public void printHash(){
		for( int i = 0 ; i < this.nonterminals.size() ; i++ )
		{
			System.out.println(this.nonterminals.get(i).detailedToString());
		}
		System.out.println();
	}

	public void printFirst(){
		for(nonterminal nonterminal : this.nonterminals)
		{
			nonterminal.printFirst();
		}	
		System.out.println();	
	}
	
	public void buildParseTable(){
		this.stepIIa();
		this.stepIIb();
	}

	public LLtable getParseTable(){
		return this.parseTable;
	}

	public void printFollow(){
		for(nonterminal nonterminal : this.nonterminals)
		{
			nonterminal.printFollow();
		}
		System.out.println();		
	}
	
	public void printParseTable(){
		for(nonterminal nonterminal : this.nonterminals){
			System.out.println("NONTERM: "+nonterminal.getSymbol());
			for(terminal terminal : this.terminals)
			{
				System.out.println("  TERM: "+terminal.getSymbol());
				System.out.println("    "+this.parseTable.get(nonterminal, terminal));
			}
			System.out.println();
		}
		System.out.println();
	}

	public void stepIIa(){
		this.convertHashTablesToLists();
			if(LLdriver.verboseOn()){
				System.out.println("Current Grammar Rules:");
				this.printHash();
			}

		this.removeSelfLeftRecursion();
			if(LLdriver.verboseOn()){
				System.out.println("Recursion Removed; New Grammar Rules:");
				this.printHash();
			}

		this.removeCommonPrefixes();
			if(LLdriver.verboseOn()){
				System.out.println("Common Prefixes Removed; New Grammar Rules:");
				this.printHash();
			}
	}

	public void convertHashTablesToLists(){
		this.nonterminals.addAll(this.nonterminalHT.values());
		this.terminals.addAll(this.terminalHT.values());
		// no longer need hash tables
		this.nonterminalHT.clear();
		this.terminalHT.clear();
	}
	
	public void removeCommonPrefixes(){
		LinkedList<nonterminal> newRules;
		for(int i = 0 ; i < this.nonterminals.size() ; i++)		// can't do for each since list is changing
		{
			newRules = this.nonterminals.get(i).removeCommonPrefixes();
			if(newRules != null){
				this.nonterminals.addAll(newRules);
			}
		}
	}

	public void removeSelfLeftRecursion(){
		nonterminal newRule, current;
		for(int i = 0 ; i < this.nonterminals.size() ; i++)
		{
			current = this.nonterminals.get(i);
			newRule = current.removeSelfLeftRecursion();
			if(newRule != null){
				this.nonterminals.addLast(newRule);
			}
		}
	}

	public void stepIIb(){
		this.constructFirst();
			if(LLdriver.verboseOn()){
				System.out.println("Constructed First Set:");
				this.printFirst();
			}

		this.constructFollow();
			if(LLdriver.verboseOn()){
				System.out.println("Constructed Follow Set:");
				this.printFollow();
			}

		this.createParseTable();
	}

	public void constructFirst(){
		boolean wasChanged = true;
		while(wasChanged){
			wasChanged = false;
			for(nonterminal nonterminal : this.nonterminals){
				wasChanged = nonterminal.constructFirst(wasChanged);
			}
		}
	}

	public void constructFollow(){
		boolean wasChanged = true;
		while(wasChanged){
			wasChanged = false;
			for(nonterminal nonterminal : this.nonterminals){
				wasChanged = nonterminal.constructFollow(wasChanged);
			}
		}
	}

	public void createParseTable(){		
		this.terminals.remove(epsilon.getEpsilonRule());
		for(nonterminal nonterminal : this.nonterminals){
			for(terminal terminal : this.terminals){
				this.parseTable.put(nonterminal, terminal, nonterminal.getProductionRuleForTerminal(terminal));
			}
			if(nonterminal.getProductionRuleForTerminal(epsilon.getEpsilonRule()) != null){
				LinkedList<terminal> followList = nonterminal.getFollow();
				for(terminal followEntry : followList){
					this.parseTable.put(nonterminal, followEntry, epsilon.getEpsilonList());
				}
			}
		}
	}

	public void saveParseTable(String fileName) throws IOException{
	    FileWriter fstream = new FileWriter(fileName);
	    BufferedWriter out = new BufferedWriter(fstream);

	    for(terminal terminal : this.terminals){
			out.write(","+terminal.getSymbol());
		}
	    out.write("\n");

	    LinkedList<Rule> productionRule;
		for(nonterminal nonterminal : this.nonterminals){
		    out.write(nonterminal.getSymbol());
			for(terminal terminal : this.terminals){
				productionRule = this.parseTable.get(nonterminal,terminal);
				if(productionRule == null)
					out.write(",");
				else
					out.write(","+productionRule.toString().replace(",", " "));
			}
		    out.write("\n");
		}
	    out.close();
	}
}
